package org.oimwrapper.connectors.common;

import Thor.API.*;
import Thor.API.Operations.*;
import com.thortech.xl.dataobj.*;
import java.util.*;

import org.oimwrapper.connectors.*;

/**
 * This tasks allows for a scheduled task to be run over multiple IT resource instances,
 * and for the delete recon to be run across the entire dataset.   This tasks looks for all IT   
 * resources of the specified type, and uses all of those instances where the disable parameter 
 * on the IT resource is not set to <code>true</code>.<p>  
 *
 * This task expects the following parameters, in addition to the standard
 * parameters in the {@link OIMReconTaskBase} class.
 * <ul>
 * <li><code>ConcurrentSources</code> The number of concurrent sources to recon against.</li>
 * <li><code>ITResourceField</code> If the target resource has multiple IT resource fields, this is the 
 *                                  name of the IT resource field that the IT resource of the type 
 *                                  listed is stored in.</li>
 * <li><code>ITResourceProperty</code> The task property under which the recon task expects to find the 
 *                                     IT resource name, for example ITResourceName.</li>
 * <li><code>ITResourceType</code> The type of IT resource to search for use by for this tasks.</li>
 * <li><code>ReconClass</code> The Java class name of the recon task that needs to be run over multiple IT 
 *                             resources.</li>
 * </ul>
 */
public class OIMMultipleSourceRecon extends OIMReconTaskBase {
	
	/**
	 * True if resources instances on OIM no longer present on the target system should be removed.
	 */
	private boolean deleteRecon;
	
	/**
	 * The list of records to return.
	 */
	private List records;

	/**
	 * The default constructor.
	 */
	public OIMMultipleSourceRecon() {
		super("OIMWRAPPER.TASK");
	}

	/**
	 * This method is called once per task, and should initialize and open a connection to the target system.
	 * 
	 * @exception Exception
	 */
	public void openConnection() throws Exception {
		logger.debug("Entering multipleSourceRecon.openConnection()");
		deleteRecon = getBooleanAttribute("DeleteOldRecords", false);
		disableDeleteRecon();
		logger.debug("Exiting multipleSourceRecon.openConnection()");
	}

	/**
	 * This method is called once per task, and should close the open connection to the target system.
	 *
	 * @exception Exception
	 */
	public void closeConnection() throws Exception {
		logger.debug("Entering multipleSourceRecon.closeConnection()");
		logger.debug("Exiting multipleSourceRecon.closeConnection()");
	}

	/**
	 * This method is called once to retrieve a List of records on the target system.   The objects in this 
	 * list are consumed by the <code>getSingleValuedData()</code> and <code>getMultiValuedData()</code>
	 * methods, and not directly used by any other methods.
	 * 
	 * @return A List of records on the target system.
	 * @exception Exception
	 */
	public List getRecords() throws Exception {
		logger.debug("Entering multipleSourceRecon.getRecords()");
		records = new ArrayList();
		int maxWorkers = Integer.parseInt(getDefaultAttribute("ConcurrentSources", "10"));
		Map[] itResources = getITResOfType(getCriticalAttribute("ITResourceType"));
		for (int i = 0; !isStopped() && i < itResources.length; i++) {
			String itResource = (String) itResources[i].get("Name");
			if (itResources[i].get("Disable") != null && ((String) itResources[i].get("Disable")).equalsIgnoreCase("TRUE")) {  
				logger.debug("Skipping IT Resource " + itResource + "; disable is set to true");  
				continue;  
			}
			logger.debug("Processing IT Resource " + itResource);
			startThread(maxWorkers, new worker(this, itResource));
		}
		waitForAllThreads();
		logger.debug("Exiting multipleSourceRecon.getRecords()");
		return records;
	}

	/**
	 * This method is called to convert the single-valued data in the arbitrary Record Object to a Map
	 * containing attribute names as the key, and the corresponding attribute value as the value.
	 * 
	 * @param Record An arbitrary object that contains the data for a single event.
	 * @return A Map containing the attribute value pairs from the record for all single-valued attributes.
	 * @exception Exception
	 */
	public final Map getSingleValuedData(Object Record) throws Exception {
		return ((record) Record).singleData;
	}

	/**
	 * This method is called to convert the multi-valued data in the arbitrary Record Object to a Map
	 * containing attribute names as the key, and an Array of Maps for the value.  The Maps in the 
	 * value should contain attribute value pairs for each child record, and one Map should be present
	 * in the array for each child element.
	 * 
	 * @param Record An arbitrary object that contains the data for a single event.
	 * @return A Map containing the attribute value pairs from the record for all multi-valued attributes.
	 * @exception Exception
	 */
	public final Map getMultiValuedData(Object Record) throws Exception {
		return ((record) Record).multiData;
	}
	
	/**
	 * This function can be overrided to allow for recon events to be submitted against different
	 * object types.
	 * 
	 * @param Record The record currently being processed.
	 * @return The name of the resource object to submit the event against.
	 * @exception Exception
	 */
	public final String getResourceName(Object Record) throws Exception {
		return ((record) Record).resourceName;
	}
	
	/**
	 * Stores records produced by a thread.
	 * 
	 * @param Records The records to store.
	 */
	protected final synchronized void storeRecords(List Records) {
		records.addAll(Records);
	}
	
	/**
	 * Executes a recon task for a single instance.
	 * 
	 * @param ITResource The name of the it resource to execute the task for.
	 */
	protected void executeSingleTask(String ITResource) {
		logger.debug("Entering multipleSourceRecon.executeSingleTask()");
		try {
			Class taskClass = Class.forName(getCriticalAttribute("ReconClass"), true, tcADPClassLoader.getClassLoader());
			OIMReconTaskBase task = (OIMReconTaskBase) taskClass.newInstance();
			logger.debug("Initializing task: " + task.getClass().getName());
			task.setParent(this);
			task.init();
			Properties attributes = getTaskAttributeMap();
			String itResourceProperty = getDefaultAttribute("ITResourceProperty", "ITResourceName");
			attributes.setProperty(itResourceProperty, ITResource);
			task.setTaskAttributeMap(attributes);
			try {
				task.openConnection();
				try {
					List rawRecords = task.getRecords();
					List processedRecords = new ArrayList();
					for (Iterator iterator = rawRecords.iterator(); !isStopped() && iterator.hasNext(); ) {
						Object rawRecord = iterator.next();
						Map singleData = task.getSingleValuedData(rawRecord);
						Map multiData = task.getMultiValuedData(rawRecord);
						String resourceName = task.getResourceName(rawRecord);
						processedRecords.add(new record(resourceName, singleData, multiData));
					}
					storeRecords(processedRecords);
					try {
						if (!isStopped() && deleteRecon) {
							task.logger.debug("Performing deletion reconciliation for " + ITResource);
							boolean demoMode = getBooleanAttribute("DemoMode", false);
							Map data = new HashMap();
							for (Iterator iterator = processedRecords.iterator(); iterator.hasNext(); ) {
								record item = (record) iterator.next();
								List items = (List) data.get(item.resourceName);
								if (items == null) {
									items = new ArrayList();
									data.put(item.resourceName, items);
								}
								items.add(item.singleData);
							}
							tcReconciliationOperationsIntf reconOp = (tcReconciliationOperationsIntf) getUtility(tcReconciliationOperationsIntf.class);
							for (Iterator iterator = data.keySet().iterator(); iterator.hasNext(); ) {
								String resource = (String) iterator.next();
								List items = (List) data.get(resource);
								Set outOfScopeAccounts = getOutOfScopeAccounts(resource, ITResource);
								if (outOfScopeAccounts == null) {
									task.logger.warn("Skipping delete recon for " + resource);
									continue;
								}
								task.logger.debug("Checking " + Integer.toString(items.size()) + " objects for deletion");
								if (!isStopped()) {
									Set matchedAccounts = new HashSet();
									if (items.size() > 0) {
										Map[] deleteCheckArray = task.prepareDeletetionDetectionData(resource, items);
										matchedAccounts = reconOp.provideDeletionDetectionData(resource, deleteCheckArray);
									}
									matchedAccounts.addAll(outOfScopeAccounts);
									matchedAccounts.addAll(getAccountsToSkip(resource));
									tcResultSet resourcesToRemove = reconOp.getMissingAccounts(resource, matchedAccounts);
									if (task.logger.isDebugEnabled() && Arrays.asList(resourcesToRemove.getColumnNames()).contains("Process Instance.Key")) {
										for (int i = 0; i < resourcesToRemove.getRowCount(); i++) {
											resourcesToRemove.goToRow(i);
											long processInstanceKey = resourcesToRemove.getLongValue("Process Instance.Key");
											task.logger.debug("Generating delete recon event for " + Long.toString(processInstanceKey));
											task.printDebugDump(getProcessFormValues(processInstanceKey));
										}
									}
									if (!isStopped() && !resourcesToRemove.isEmpty() && !demoMode) {
										long[] events = reconOp.deleteDetectedAccounts(resourcesToRemove);
										for (int i = 0; i < events.length; i++) {
											tallySuccess(events[i]);
										}
										task.logger.info("Submitted " + Integer.toString(events.length) + " events to delete resources");
									}
								}
							}
						} 
					} catch(Exception e) {
						tallyFailure();
						throw e;
					}
				} finally {
					task.closeConnection();
				}
			} catch (Exception e) {
				OIMExceptionHandler.handleException(task.logger, e, getClass());
			}
		} catch (Exception e) {
			OIMExceptionHandler.handleException(logger, e, getClass());
		}
		logger.debug("Exiting multipleSourceRecon.executeSingleTask()");
	}
	
	/**
	 * Gets a set of process instance keys that should be excluded from the delete recon processing
	 * for a particular IT resource.
	 * 
	 * @return A set of process instance keys that should be excluded from the delete recon, or null
	 *         if the data to do the matchign is not present.
	 * @exception Exception
	 */
	protected Set getOutOfScopeAccounts(String Resource, String ITResource) throws Exception {
		logger.debug("Entering multipleSourceRecon.getOutOfScopeAccounts()");
		Set result = new HashSet();
		String itResourceField = null;
		String table = null;
		String query = "select orf_fieldname, prf_columnname, sdk_name from prf ";
		query += "inner join orf on prf.orf_key = orf.orf_key ";
		query += "inner join obj on orf.obj_key = obj.obj_key ";
		query += "inner join pkg on obj.obj_key = pkg.obj_key ";
		query += "inner join tos on pkg.pkg_key = tos.pkg_key ";
		query += "inner join sdk on tos.sdk_key = sdk.sdk_key ";
		query += "where orf_fieldtype='IT Resource' ";
		query += "and pkg_obj_default = '1' ";
		query += "and obj_name = '" + Resource + "'";
		logger.debug("query = " + query);
		Map[] queryResults = getOimData(query);
		if (queryResults.length == 0) {
			logger.warn("No IT Resource fields found for " + Resource);
			result = null;
		} else if (queryResults.length == 1) {
			itResourceField = (String) queryResults[0].get("Process Definition.Reconciliation Fields Mappings.ColumnName");
			table = (String) queryResults[0].get("Structure Utility.Table Name");
		} else {
			String attribute = getAttribute("ITResourceField");
			if (itResourceField != null) {
				for (int i = 0; i < queryResults.length; i++) {
					String reconAttribute = (String) queryResults[i].get("Objects.Reconciliation Fields.Name");
					String destinationColumn = (String) queryResults[i].get("Process Definition.Reconciliation Fields Mappings.ColumnName");
					if (reconAttribute.equalsIgnoreCase(attribute) || destinationColumn.equalsIgnoreCase(attribute)) {
						itResourceField = destinationColumn;
						table = (String) queryResults[i].get("Structure Utility.Table Name");
						break;
					}
				}
			}
			if (isEmpty(itResourceField)) {
				logger.warn("Multiple IT Resources found for " + Resource);
				logger.warn("Please set the task parameter ITResourceField to indicate which one should be used");
				result = null;
			}
		}
		if (itResourceField != null) {
			query = "select orc.orc_key from " + table + " ";
			query += "inner join orc on orc.orc_key = " + table + ".orc_key ";
			query += "where orc_status = 'C' ";
			query += "and " + itResourceField + " <> " + Long.toString(getItResource(ITResource));
			logger.debug("query = " + query);
			queryResults = getOimData(query);
			for (int i = 0; i < queryResults.length; i++) {
				result.add(Long.valueOf((String) queryResults[i].get("Process Instance.Key")));
			}
		}
		if (logger.isDebugEnabled() && result != null) {
			logger.debug("result = " +  result.toString());
		}
		logger.debug("Exiting multipleSourceRecon.getOutOfScopeAccounts()");
		return result;
	}
	
	/**
	 * Internal class to store a record.
	 */
	private class record {
		
		/**
		 * The name of the resource.
		 */
		protected String resourceName;
		
		/**
		 * Single valued data.
		 */
		protected Map singleData;
		
		/**
		 * Multi-valued data.
		 */
		protected Map multiData;
		
		/**
		 * Default Constructor.
		 * 
		 * @param ResourceName The name of the resource.
		 * @param SingleData Single valued data.
		 * @param MultiData Multi-valued data.
		 */
		protected record(String ResourceName, Map SingleData, Map MultiData) {
			resourceName = ResourceName;
			singleData = SingleData;
			multiData = MultiData;
		}
	}
	
	/**
	 * Worker Thread.
	 */
	private class worker extends OIMTaskThreadBase {
		
		/**
		 * The invoking recon task.
		 */
		private OIMMultipleSourceRecon task;
		
		/**
		 * The IT Resource to process.
		 */
		private String itResource;
		
		/**
		 * Default Constructor.
		 * 
		 * @param ReconTask The invoking recon task.
		 * @param ITResource The IT Resource to process.
		 */
		private worker(OIMMultipleSourceRecon Task, String ITResource) {
			super(Task);
			task = Task;
			itResource = ITResource;
		}
		
		/**
		 * Processes and submits a single recon event.
		 */
		public void runOimThread() {
			task.executeSingleTask(itResource);
		}
	}
}